<?php


namespace App\Services;


use App\Models\Config;
use App\Models\User;
use App\Models\UserSecret;
use App\Utils\Code;
use App\Utils\Tencent\BizDataCrypt;
use App\Utils\Xml;
use Http;
use Illuminate\Support\Str;

class WechatService extends BaseService
{
    /**
     * access_token
     * @var string
     */
    private $accessToken = 'https://api.weixin.qq.com/sns/oauth2/access_token';

    /**
     * jscode_to_session
     * @var string
     */
    private $jsCodeSession = 'https://api.weixin.qq.com/sns/jscode2session';

    /**
     * 微信登录
     * @param array $params
     * @return mixed
     * @throws \Exception
     */
    public function login(array $params) {
        // 验证请求参数
        $rules = [
            'mode' => ['bail', 'required', 'in:xcx,gzh'],
            'code' => ['bail', 'required', 'string']
        ];
        $validator = \Validator::make($params, $rules, [], [
            'mode' => '登录类型',
            'code' => '微信CODE编码'
        ]);
        if ($validator->fails()) {
            throw new \Exception($validator->errors()->first(), Code::PARAMETER_ERROR);
        }
        if ($params['mode'] == 'gzh') {
            return $this->gzhLogin($params['code']);
        } else {
            return $this->xcxLogin($params['code']);
        }
    }

    /**
     * 微信小程序登录
     */
    private function xcxLogin($code) {
        $wechat = $this->jsCodeSession($code);
        return \DB::transaction(function () use ($wechat) {
            $info = UserSecret::where('mode', 'wx_xcx')
                ->where('secret', $wechat['openid'])->first();
            if (!$info) {
                $info = new UserSecret();
                $info->mode = 'wx_xcx';
                $info->user_id = null;
                $info->secret = $wechat['openid'];
            }
            if (isset($wechat['unionid']) && $wechat['unionid']) {
                $info->unionid = $wechat['unionid'];
                if ($info->user_id) {
                    \DB::table('user_secrets')->where('unionid', $info->unionid)
                        ->whereNull('user_id')->update(['user_id' => $info->user_id]);
                }
            }
            $info->session = $wechat['session_key'];
            $info->save();
            if ($info->user_id) {
                return (new UserService())->show($info->user_id, false, true, true, $info->mode);
            } else {
                return ['openid' => $wechat['openid']];
            }
        });
    }

    /**
     * 通过code获取微信小程序登录信息
     */
    private function jsCodeSession($code, $appid = null, $secret = null) {
        if (!$appid || !$secret) {
            $config = Config::where('key', 'wechat_xcx')->first();
            if (!isset($config->params['secret']) || !$config->params['secret']) {
                throw new \Exception('微信应用参数未配置', Code::LACK_OF_CONDITIONS);
            }
            $appid = $config->value;
            $secret = $config->params['secret'];
        }
        $result = Http::get($this->jsCodeSession, [
            'appid' => $appid,
            'secret' => $secret,
            'js_code' => $code,
            'grant_type' => 'authorization_code'
        ]);
        $result = json_decode($result->body(), true);
        if (isset($result['errmsg'])) {
            if (isset($result['errcode']) && $result['errcode'] == 40163) {
                throw new \Exception('临时CODE已失效', Code::LACK_OF_CONDITIONS);
            }
            throw new \Exception($result['errmsg'], Code::LACK_OF_CONDITIONS);
        }
        return $result;
    }
    
    // 微信H5页面登录
    private function gzhLogin(string $code) {
        return \DB::transaction(function () use ($code) {
            $wechat = $this->gzhLoginAccessToken('gzh', $code);
            Code::log('微信登录：'.json_encode($wechat));
            $wxUser = $this->loginWithUserInfo($wechat['openid'], $wechat['access_token']);
            if ($wxUser['nickname'] == '微信用户') {
                return false;
            }
            $info = UserSecret::where('mode', 'wx_gzh')->where('secret', $wxUser['openid'])->first();
            // 已经创建了用户，直接返回用户信息
            if ($info && $info->user_id) {
                $userInfo = User::where('user_id', $info->user_id)->first();
                if (!$userInfo->name) {
                    $userInfo->name = $wxUser['nickname'];
                    $userInfo->avatar = $wxUser['headimgurl'];
                    $userInfo->save();
                }
                return (new UserService())->show($info->user_id, false, true, true, 'wx_gzh');
            // 没有记录过openid，则记录openid
            } elseif (!$info) {
                $info = new UserSecret();
                $info->mode = 'wx_gzh';
                $info->secret = $wxUser['openid'];
                $info->save();
            }
            // 以下是没有在h5注册过用户时的流程
            if (!$info->user_id) {
                $userInfo = (new UserService())->store([
                    'is_admin' => 0,
                    'status' => 1,
                    'name' => $wxUser['nickname'],
                    'avatar' => $wxUser['headimgurl']
                ]);
                $info->user_id = $userInfo->user_id;
            }
            $info->save();
            return (new UserService())->show($info->user_id, false, true, true, 'wx_gzh');
        });
    }

    private function gzhLoginAccessToken($mode, $code, $appid = null, $secret = null) {
        if (!$appid || !$secret) {
            $config = Config::where('key', 'wechat_'.$mode)->first();
            if (!isset($config->params['secret']) || !$config->params['secret']) {
                throw new \Exception('微信应用参数未配置', Code::LACK_OF_CONDITIONS);
            }
            $appid = $config->value;
            $secret = $config->params['secret'];
        }
        $result = Http::get($this->accessToken, [
            'appid' => $appid,
            'secret' => $secret,
            'code' => $code,
            'grant_type' => 'authorization_code'
        ]);
        $result = json_decode($result->body(), true);
        if (isset($result['errmsg'])) {
            if (isset($result['errcode']) && $result['errcode'] == 40163) {
                throw new \Exception('临时CODE已失效', Code::LACK_OF_CONDITIONS);
            }
            throw new \Exception($result['errmsg'], Code::LACK_OF_CONDITIONS);
        }
        return $result;
    }

    private function loginWithUserInfo($openid, $accessToken) {
        $result = Http::get('https://api.weixin.qq.com/sns/userinfo', [
            'access_token' => $accessToken,
            'openid' => $openid,
            'lang' => 'zh_CN'
        ]);
        return json_decode($result->body(), true);
    }

    /**
     * 手机绑定
     * @param array $params
     * @return \Illuminate\Database\Eloquent\Builder|\Illuminate\Database\Eloquent\Model|object|\Tymon\JWTAuth\Contracts\JWTSubject|null
     * @throws \Exception
     */
    public function bindPhone(array $params) {
        // 验证请求参数
        $rules = [
            'mode' => ['bail', 'required', 'in:xcx,gzh'],
            'openid' => ['bail', 'required', 'string'],
            'iv' => ['bail', 'required', 'string'],
            'encrypted_data' => ['bail', 'required', 'string']
        ];
        $validator = \Validator::make($params, $rules, [], [
            'mode' => '登录类型',
            'openid' => '微信身份标识',
            'iv' => '初始向量 iv',
            'encrypted_data' => '加密字符串 encrypted_data',
        ]);
        if ($validator->fails()) {
            throw new \Exception($validator->errors()->first(), Code::PARAMETER_ERROR);
        }
        $secretInfo = UserSecret::where('mode', 'wx_'.$params['mode'])
            ->where('secret', $params['openid'])->first();
        if (!$secretInfo) {
            throw new \Exception('不存在的 openid 信息', Code::EMPTY_DATA);
        } elseif ($secretInfo->user_id) {
            throw new \Exception('该微信已经绑定有手机号码', Code::LACK_OF_CONDITIONS);
        }
        $crypt = new BizDataCrypt(Config::where('key', 'wechat_'.$params['mode'])
            ->where('type', 0)->value('value'), $secretInfo->session);
        $phone = ($crypt->decryptData($params['iv'], $params['encrypted_data']))['purePhoneNumber'];

        // 验证用户信息
        $userInfo = User::where('phone', $phone)->first();
        if (!$userInfo) {
            $userInfo = (new UserService())->store([ 'phone' => $phone ]);
        } elseif ($userInfo->secret()->where('mode', $secretInfo->mode)->exists()) {
            throw new \Exception('该手机已绑定其他微信', Code::LACK_OF_CONDITIONS);
        };
        $secretInfo->user_id = $userInfo->user_id;
        $secretInfo->save();
        return (new UserService())
            ->show($userInfo->user_id, false, true, true, $secretInfo->mode);
    }

    /**
     * 获取用户凭证
     * @param $mode
     * @param null $config
     * @return mixed
     * @throws \Exception
     */
    public function gzhAccessToken($mode, $config = null) {
        if (!$config) {
            $config = Config::where('key', 'wechat_' . $mode)->first()->toArray();
            if (!$config['value'] || !isset($config['params']['secret']) || !$config['params']['secret']) {
                throw new \Exception('应用通讯参数未配置', Code::LACK_OF_CONDITIONS);
            }
        }
        $cacheMark = 'wechat_access_token_' . $config['value'];
        if (!$access_token = \Cache::get($cacheMark, null)) {
            $response = json_decode(Http::get('https://api.weixin.qq.com/cgi-bin/token', [
                'appid' => $config['value'],
                'secret' => $config['params']['secret'],
                'grant_type' => 'client_credential',
            ]), true);
            if (isset($response['expires_in']) && $response['expires_in']) {
                \Cache::put($cacheMark, $response['access_token'], ($response['expires_in'] - 200));
                $access_token = $response['access_token'];
            } elseif (isset($response['errmsg']) && $response['errmsg']) {
                throw new \Exception($response['errmsg'], Code::REMOTE_SERVER);
            } else {
                throw new \Exception('获取access_token失败', Code::REMOTE_SERVER);
            }
        }
        return $access_token;
    }

    /**
     * 获取用户信息
     * @param $openId
     * @param null $config
     * @return array|mixed
     * @throws \Exception
     */
    public function gzhUserInfo($openId, $config = null) {
        if (!$config) {
            $config = Config::where('key', 'wechat_gzh')->first()->toArray();
            if (!$config['value'] || !isset($config['params']['secret']) || !$config['params']['secret']) {
                throw new \Exception('公众号通讯参数未配置', Code::LACK_OF_CONDITIONS);
            }
        }
        return Http::get('https://api.weixin.qq.com/cgi-bin/user/info', [
            'access_token' => $this->gzhAccessToken('gzh', $config),
            'openid' => $openId,
            'lang' => 'zh_CN'
        ])->json();
    }

    /**
     * 事件响应
     * @param array $params
     * @param $content
     * @return string
     */
    private function gzhResponse(array $params, $content){
        $foUser = $params['FromUserName'];
        $toUser = $params['ToUserName'];
        $msgType = 'text';
        $createTime = time();
        $xmlText = "<xml><ToUserName><![CDATA[%s]]></ToUserName><FromUserName><![CDATA[%s]]></FromUserName><CreateTime>%s</CreateTime><MsgType><![CDATA[%s]]></MsgType><Content><![CDATA[%s]]></Content></xml>";
        return sprintf($xmlText,$foUser,$toUser,$createTime,$msgType,$content);
    }

    /**
     * 公众号消息通知
     * @param array $params
     * @param $xml
     * @return string
     * @throws \Exception
     */
    public function callbackGzh(array $params, $xml) {
        $config = Config::where('key', 'wechat_gzh')->first()->toArray();
        if (!$config['value'] || !isset($config['params']['secret']) || !$config['params']['secret']) {
            throw new \Exception('公众号通讯参数未配置', Code::LACK_OF_CONDITIONS);
        }
        $config['params']['token'] = isset($config['params']['token']) ? $config['params']['token'] : null;
        $config['params']['encodingAESKey'] = isset($config['params']['encodingAESKey']) ? $config['params']['encodingAESKey'] : null;
        if (!$config['params']['token']) {
            throw new \Exception('公众号令牌（Token）未配置', Code::LACK_OF_CONDITIONS);
        } elseif (!$config['params']['encodingAESKey']) {
            throw new \Exception('公众号消息加解密密钥（EncodingAESKey）未配置', Code::LACK_OF_CONDITIONS);
        }
        $pc = new \WXBizMsgCrypt($config['params']['token'], $config['params']['encodingAESKey'], $config['value']);
        if (isset($params['msg_signature'])) {
            if (($res = $pc->decryptMsg($params['msg_signature'], $params['timestamp'], $params['nonce'], $xml, $data)) != 0) {
                if ($res = -40001) {
                    throw new \Exception('签名验证错误', Code::LACK_OF_CONDITIONS);
                } elseif ($res = -40002) {
                    throw new \Exception('xml解析失败', Code::LACK_OF_CONDITIONS);
                } elseif ($res = -40003) {
                    throw new \Exception('sha加密生成签名失败', Code::LACK_OF_CONDITIONS);
                } elseif ($res = -40004) {
                    throw new \Exception('encodingAesKey 非法', Code::LACK_OF_CONDITIONS);
                } elseif ($res = -40005) {
                    throw new \Exception('appid 校验错误', Code::LACK_OF_CONDITIONS);
                } elseif ($res = -40006) {
                    throw new \Exception('aes 加密失败', Code::LACK_OF_CONDITIONS);
                } elseif ($res = -40007) {
                    throw new \Exception('aes 解密失败', Code::LACK_OF_CONDITIONS);
                } elseif ($res = -40008) {
                    throw new \Exception('解密后得到的buffer非法', Code::LACK_OF_CONDITIONS);
                } elseif ($res = -40009) {
                    throw new \Exception('base64加密失败', Code::LACK_OF_CONDITIONS);
                } elseif ($res = -40010) {
                    throw new \Exception('base64解密失败', Code::LACK_OF_CONDITIONS);
                } elseif ($res = -40011) {
                    throw new \Exception('生成xml失败', Code::LACK_OF_CONDITIONS);
                } else {
                    throw new \Exception('通讯遇到错误', Code::LACK_OF_CONDITIONS);
                }
            }
        } else {
            $data = $xml;
        }
        $data = Xml::instance()->xmlToObject($data);
        $responseText = '';
        if (isset($data['MsgType'])) {
            if (isset($data['Event'])) {
                // 公众号关注事件
                if (in_array($data['Event'], ['subscribe', 'unsubscribe'])) {
                    $this->gzhSubscribe($data['FromUserName'], $data['Event'], $config);
                    if ($data['Event'] == 'subscribe') {
                        $responseText .= "感谢您的关注";
                    }
                } elseif ($data['Event'] == 'CLICK') {
                    if ($data['EventKey'] == 'SELFMENU_CONTACT') {
                        $responseText .= '点击菜单';
                    }
                } else {
                    $responseText .= '点击菜单';
                }
            } else {
                $responseText .= '点击菜单';
            }
        } else {
            $responseText .= '点击菜单';
        }
        return $this->gzhResponse($data, $responseText);
    }

    /**
     * 公众号关注事件
     * @param $openid
     * @param $action
     * @param null $config
     * @return bool
     * @throws \Exception
     */
    private function gzhSubscribe($openid, $action, $config = null){
        $userInfo = $this->gzhUserInfo($openid, $config);
        if (!$secret = UserSecret::where('mode', 'wx_gzh')->where('secret', $openid)->first()) {
            $secret = new UserSecret();
            $secret->mode = 'wx_gzh';
            $secret->secret = $openid;
        };
        return \DB::transaction(function () use ($userInfo, $secret, $action){
            if (isset($userInfo['unionid']) && $userInfo['unionid']) {
                $secret->unionid = $userInfo['unionid'];
                if (!$secret->user_id && ($userId = \DB::table('user_secrets')
                        ->where('unionid', $secret->unionid)->whereNotNull('user_id')->value('user_id'))) {
                    $secret->user_id = $userId;
                } elseif ($secret->user_id && \DB::table('user_secrets')
                        ->where('unionid', $secret->unionid)->whereNull('user_id')->exists()) {
                    \DB::table('user_secrets')
                        ->where('unionid', $secret->unionid)->whereNull('user_id')->update(['user_id', $secret->user_id]);
                }
            }
            $secret->is_keep = ($action == 'subscribe' ? 1 : 0);
            return $secret->save();
        });
    }

    /**
     * 素材列表
     * @return mixed
     * @throws \Exception
     */
    public function gzhMaterialList() {
        return json_decode(Http::get('https://api.weixin.qq.com/cgi-bin/material/batchget_material', [
            'access_token' => $this->gzhAccessToken('gzh')
        ]), true);
    }

    /**
     * 公众号菜单信息
     * @return mixed
     * @throws \Exception
     */
    public function gzhMenuInfo() {
        return json_decode(Http::get('https://api.weixin.qq.com/cgi-bin/menu/get', [
            'access_token' => $this->gzhAccessToken('gzh')
        ]), true);
    }

    /**
     * 公众号菜单更新
     * @param array $params
     * @return bool
     * @throws \Throwable
     */
    public function gzhMenuSave(array $params) {
        $url = 'https://api.weixin.qq.com/cgi-bin/menu/create?access_token=' . $this->gzhAccessToken('gzh');
        $response = Http::asJson()->send('POST', $url, [
            'body' => json_encode($params, JSON_UNESCAPED_UNICODE)
        ])->json();
        if (isset($response['errcode']) && $response['errcode'] == 0) {
            return true;
        } else {
            Code::log(json_encode($response, JSON_UNESCAPED_UNICODE));
            throw new \Exception('自定义菜单更新失败', Code::LACK_OF_CONDITIONS);
        }
    }

    /**
     * 公众号菜单更新
     * @param array $params
     * @return array
     * @throws \Throwable
     */
    public function gzhJsconfig(array $params = []) {
        $config = Config::where('key', 'wechat_gzh')->first();
        if (!isset($config->params['secret']) || !$config->params['secret']) {
            throw new \Exception('微信应用参数未配置', Code::LACK_OF_CONDITIONS);
        }
        $appid = $config->value;
        if (!isset($params['url']) || !$params['url']) {
            throw new \Exception('缺少url参数', Code::PARAMETER_ERROR);
        }
        $cacheMark = 'wechat_jsapi_ticket_' . $appid;
        if (!$ticket = \Cache::get($cacheMark, null)) {
            $url = 'https://api.weixin.qq.com/cgi-bin/ticket/getticket?access_token=' . $this->gzhAccessToken('gzh') . '&type=jsapi';
            $response = Http::get($url);
            $response = json_decode($response->body(), true);
            if (isset($response['expires_in']) && $response['expires_in']) {
                \Cache::put($cacheMark, $response['ticket'], ($response['expires_in'] - 200));
                $ticket = $response['ticket'];
            } elseif (isset($response['errmsg']) && $response['errmsg']) {
                throw new \Exception($response['errmsg'], Code::REMOTE_SERVER);
            } else {
                throw new \Exception('获取jsapi_ticket失败', Code::REMOTE_SERVER);
            }
        }
        $nonce = Str::random();
        $timestamp = time();
        $data = [
            'url' => $params['url'],
            'jsApiList' => ['scanQRCode'],
            'nonceStr' => $nonce,
            'timestamp' => $timestamp,
            'appId' => $appid,
            'signature' => sha1(sprintf(
                'jsapi_ticket=%s&noncestr=%s&timestamp=%s&url=%s',
                $ticket,
                $nonce,
                $timestamp,
                $params['url']
            )),
        ];
        return $data;
    }
}
